---
title: "Forms"
group: "Client Side Usage"
groupOrder: 5
---

Learn how to use ZSA with forms.

## Basic Form

You can use a server action directly with a regular form. Set the `type` to `"formData"` to indicate that the input is a `FormData` object.

```typescript title="actions.ts"
"use server"

import z from "zod"
import { createServerAction } from "zsa"

export const produceNewMessage = createServerAction()
  .input(
    z.object({
      name: z.string().min(5),
    }),
    {
      type: "formData", // [!code highlight]
    }
  )
  .handler(async ({ input }) => {
    await new Promise((resolve) => setTimeout(resolve, 500))
    return "Hello, " + input.name
  })
```

Then on the client you can pass the `action` prop to the form to handle the form submission.

```tsx title="form-example.tsx"
"use client"

import { produceNewMessage } from "./actions";

export default function FormExample() {
    return (
        <form
            action={produceNewMessage} // [!code highlight]
        >
            <label>
                Name:
                <input type="text" name="name" required />
            </label>
            <button type="submit">Submit</button>
        </form>
    );
}
```

### Submit with `useServerAction`

However if you do it this way you will not have access to the `data` and `error` variables. To get the data and error you can use the `useServerAction` hook.

```tsx title="basic-form-example.tsx"
"use client"

import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { useServerAction } from "zsa-react"
import { produceNewMessage } from "./actions"

export const BasicForm = () => {
  const { isPending, execute, isSuccess, data, isError, error } = // [!code highlight]
    useServerAction(produceNewMessage) // [!code highlight]

  return (
    <Card className="not-prose">
      <CardHeader>
        <CardTitle>Form Example</CardTitle>
      </CardHeader>
      <CardContent className="flex flex-col gap-4">
        <form
          className="flex flex-col gap-4"
          onSubmit={async (event) => {
            event.preventDefault()
            const form = event.currentTarget

            const formData = new FormData(form) // [!code highlight]
            const [data, err] = await execute(formData) // [!code highlight]

            if (err) {
              // handle error
              return
            }

            form.reset()
          }}
        >
          <Input type="text" name="name" placeholder="Enter your name..." />
          <Button className="w-full" type="submit" disabled={isPending}>
            Submit
          </Button>
          {isPending && <div>Loading...</div>}
          {isSuccess && <div>Success: {JSON.stringify(data)}</div>}
          {isError && <div>Error: {JSON.stringify(error.fieldErrors)}</div>}
        </form>
      </CardContent>
    </Card>
  )
}

```

Here is the result:

<ExampleComponent id="basic-form" />

In this example, we create a basic form and handle the form submission manually. We use the `useServerAction` hook to execute the ZSA action and handle the success and error states accordingly.

### Form Actions

`useServerAction` also supports executing a form action directly. This is useful when you want to handle form submissions in your React components.

To do this, you can use the `executeFormAction` function passed back by `useServerAction` hook.

First, make sure you have the input type set to `"formData"` in your server action.

```typescript title="actions.ts"
"use server"

import z from "zod"
import { createServerAction } from "zsa"

export const produceNewMessage = createServerAction()
  .input(
    z.object({
      name: z.string().min(5),
    }),
    {
      type: "formData", // [!code highlight]
    }
  )
  .handler(async ({ input }) => {
    await new Promise((resolve) => setTimeout(resolve, 500))
    return "Hello, " + input.name
  })
```

Then, you can use the `executeFormAction` function to execute the form action.

```tsx title="form-example.tsx"
"use client"

import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { useServerAction } from "zsa-react"
import { produceNewMessage } from "./actions"

export const ExecuteFormAction = () => {
  const { isPending, executeFormAction, isSuccess, data, isError, error } = // [!code highlight]
    useServerAction(produceNewMessage) // [!code highlight]

  return (
    <Card className="not-prose">
      <CardHeader>
        <CardTitle>Form Example</CardTitle>
      </CardHeader>
      <CardContent className="flex flex-col gap-4">
        <form className="flex flex-col gap-4" action={executeFormAction}> // [!code highlight]
          <Input type="text" name="name" placeholder="Enter your name..." />
          <Button className="w-full" type="submit" disabled={isPending}>
            Submit
          </Button>
          {isPending && <div>Loading...</div>}
          {isSuccess && <div>Success: {JSON.stringify(data)}</div>}
          {isError && <div>Error: {JSON.stringify(error.fieldErrors)}</div>}
        </form>
      </CardContent>
    </Card>
  )
}
```

Here is the result:

<ExampleComponent id="execute-form-action" />

## React Hook Form

The `useServerAction` hook from the `zsa-react` library allows you to easily integrate ZSA with the popular `react-hook-form` library. Here's an example of how to use it:

```typescript title="actions.ts"
"use server"

import z from "zod"
import { createServerAction } from "zsa"

export const produceNewMessage = createServerAction()
  .input(
    z.object({
      name: z.string().min(5),
    }),
  )
  .handler(async ({ input }) => {
    await new Promise((resolve) => setTimeout(resolve, 500))
    return "Hello, " + input.name
  })
```

Then on the client side, you can use the `useServerAction` hook to execute the action and handle the result.

```tsx title="react-hook-form.tsx"
"use client"

import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { useServerAction } from "zsa-react"
import { produceNewMessage } from "./actions"

const formSchema = z.object({
  name: z.string().min(2, {
    message: "Name must be at least 2 characters.",
  }),
})

export function ReactHookForm() {
  const { isPending, execute, data, error } = useServerAction(produceNewMessage) // [!code highlight]

  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      name: "",
    },
  })

  async function onSubmit(values: z.infer<typeof formSchema>) {
    const [data, err] = await execute(values) // [!code highlight]

    if (err) {
      // show a toast or something
      return
    }

    form.reset({ name: "" })
  }

  return (
    <Card className="not-prose">
      <CardHeader>
        <CardTitle>Form Example</CardTitle>
      </CardHeader>
      <CardContent className="flex flex-col gap-4">
        <Form {...form}>
          <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
            <FormField
              control={form.control}
              name="name"
              render={({ field }) => (
                <FormItem>
                  <FormLabel>Name</FormLabel>
                  <FormControl>
                    <Input placeholder="shadcn" {...field} />
                  </FormControl>
                  <FormMessage />
                </FormItem>
              )}
            />
            <Button disabled={isPending} type="submit" className="w-full">
              {isPending ? "Saving..." : "Save"}
            </Button>
          </form>
        </Form>
        {data && <div>Message: {data}</div>}
        {error && <div>Error: {JSON.stringify(error.fieldErrors)}</div>}
      </CardContent>
    </Card>
  )
}

```

Here is the result:

<ExampleComponent id="react-hook-form" />

In this example, we define a form schema using Zod and create a form using `react-hook-form`. We then use the `useServerAction` hook to execute the ZSA action when the form is submitted. The `isPending` variable can be used to display a loading state while the action is being executed.

## useActionState

<Info>
useActionState was previously called useFormState. It is now deprecated and will be removed in a future release.
</Info>

The `useActionState` hook from ZSA allows you to perform actions and manage the action state. Make sure to set the `type` to `"state"` to indicate that the input is a (previousState, formData) tuple.

```typescript title="actions.ts"
"use server"

import z from "zod"
import { createServerAction } from "zsa"

export const produceNewMessage = createServerAction()
  .input(
    z.object({
      name: z.string().min(5),
    }),
    {
      type: "state", // [!code highlight]
    }
  )
  .handler(async ({ input }) => {
    await new Promise((resolve) => setTimeout(resolve, 500))
    return "Hello, " + input.name
  })
```

Then on the client side, you can use the `useActionState` hook to execute the action and handle the result.

```tsx title="use-action-state.tsx"
"use client"

import { useActionState } from "react";
import { produceNewMessage } from "./actions";

export default function UseActionStateExample() {
  let [[data, err], submitAction, isPending] = useActionState( // [!code highlight]
    produceNewMessage, // [!code highlight]
    [null, null] // or [initialData, null] // [!code highlight]
  ); // [!code highlight]

  return (
    <Card className="not-prose">
      <CardHeader>
        <CardTitle>Use Form State</CardTitle>
      </CardHeader>
      <CardContent className="flex flex-col gap-4">
        <form action={submitAction} className="flex flex-col gap-4"> // [!code highlight]
          <Input name="name" placeholder="Enter your name..." />
          <Button disabled={isPending}>Create message</Button>
        </form>
        {isPending && <div>Loading...</div>}
        {data && <p>Message: {data}</p>}
        {err && <div>Error: {JSON.stringify(err)}</div>}
      </CardContent>
    </Card>
  );
}
```

Here is the result:

<ExampleComponent id="use-action-state" />

---

<Note>
This example supports progressive enhancement. Give it a shot, try submitting after disabling Javascript!
</Note>

In this example, we use the `useActionState` hook to perform the `produceNewMessage` action and manage the action state. The `submitAction` function is passed to the form's `action` prop to handle the form submission. The `isPending`, `data`, and `err` variables can be used to display loading, success, and error states respectively.

### Custom State

What if you want to control the state of the form yourself? Let's see how we can do that. This example will show how to append messages to an array.

First, make sure you are using `formData` as the input type.

```typescript title="actions.ts"
"use server"

import z from "zod"
import { createServerAction } from "zsa"

const produceNewMessageAction = createServerAction()
  .input(
    z.object({
      name: z.string(),
    }),
    {
      type: "formData", // [!code highlight]
    }
  )
  .handler(async ({ input }) => {
    await new Promise((resolve) => setTimeout(resolve, 500))
    return "Hello, " + input.name
  })

export const produceNewMessage = async (
  previousState: string[],
  formData: FormData
) => {
  const [data, err] = await produceNewMessageAction(formData) // [!code highlight]

  if (err) {
    // handle error
    return previousState // [!code highlight]
  }

  return [...previousState, data] // [!code highlight]
}

```

Now we can use the `useActionState` hook to control the state of the form.

```tsx title="use-action-state-custom-state.tsx"
"use client"

import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { useActionState } from "react"
import { produceNewMessage } from "./actions"

export default function UseActionCustomStateExample() {
  let [messages, submitAction] = useActionState(produceNewMessage, [ // [!code highlight]
    "my initial message", // [!code highlight]
  ]) // [!code highlight]

  return (
    <Card className="not-prose">
      <CardHeader>
        <CardTitle>Use Action Custom State</CardTitle>
      </CardHeader>
      <CardContent className="flex flex-col gap-4">
        <form action={submitAction} className="flex flex-col gap-4"> // [!code highlight]
          <Input name="name" placeholder="Enter your name..." />
          <Button>Create message</Button>
        </form>
        <h1>Messages:</h1>
        <div>
          {messages.map((message, index) => (
            <div key={index}>{message}</div>
          ))}
        </div>
      </CardContent>
    </Card>
  )
}

```

Let's check out the result!

<ExampleComponent id="use-form-state" />

### Skip Input Parsing

What if you want to receive a raw payload, such as FormData object, and handle it yourself? This is useful for example when using the `@conform-to/react` library. Let's see how we can do that.

```typescript title="actions.ts"
"use server"

import z from "zod"
import { createServerAction } from "zsa"

export const produceNewMessage = createServerAction()
  .input(
    z.custom<FormData>(), // [!code highlight]
    {
      type: "state", // [!code highlight]
      skipInputParsing: true // [!code highlight]
    }
  )
  .handler(async ({ input }) => {
    const payload = Object.fromEntries(input)

    const result = z.object({
      name: z.string().min(5)
    }).safeParse(payload)

    if (result.error) {
      throw result.error
    }

    await new Promise((resolve) => setTimeout(resolve, 500))

    return "Hello, " + result.data.name
  })
```

Then on the client side, you can use the `useActionState` hook to execute the action and handle the result.

```tsx title="use-action-state-skip-input-parsing.tsx"
"use client"

import { useActionState } from "react";
import { produceNewMessage } from "./actions";

export default function UseActionStateSkipInputParsingExample() {
  let [[data, err], submitAction, isPending] = useActionState( // [!code highlight]
    produceNewMessage, // [!code highlight]
    [null, null] // or [initialData, null] // [!code highlight]
  ); // [!code highlight]

  return (
    <Card className="not-prose">
      <CardHeader>
        <CardTitle>Skip Input Parsing</CardTitle>
      </CardHeader>
      <CardContent className="flex flex-col gap-4">
        <form action={submitAction} className="flex flex-col gap-4"> // [!code highlight]
          <Input name="name" placeholder="Enter your name..." />
          <Button disabled={isPending}>Create message</Button>
        </form>
        {isPending && <div>Loading...</div>}
        {data && <p>Message: {data}</p>}
        {err && <div>Error: {JSON.stringify(err)}</div>}
      </CardContent>
    </Card>
  );
}
```

Here is the result:

<ExampleComponent id="use-action-state-skip-input-parsing" />

## File Upload

ZSA supports file and/or image uploads within forms. To handle file uploads, you need to specify the expected file type in the server action using `z.instanceof(File)` and include a file input in your form.

```typescript title="actions.ts"
"use server"

import { z } from "zod";
import { createServerAction } from "zsa";

export const uploadFile = createServerAction()
  .input(
    z.object({
      name: z.string().min(5),
      file: z // [!code highlight]
        .instanceof(File) // [!code highlight]
        .refine( // [!code highlight]
          (file) => file.size > 0 && file.size < 1024, // [!code highlight]
          "File size must be less than 1kb" // [!code highlight]
        ), // [!code highlight]
    }),
    {
      type: "state",
    }
  )
  .handler(async ({ input }) => {
    // Process the uploaded file
    const { name, file } = input;
    // ...

    return "File uploaded successfully!";
  });
```

On the client side, include a file input in your form:

```tsx title="file-upload-example.tsx"
"use client"

import { useActionState } from "zsa-react";
import { uploadFile } from "./actions";

export default function FileUploadExample() {
  const [[data, error], submitAction, isPending] = useActionState(uploadFile, [null, null]); // [!code highlight]

  return (
    <form action={submitAction} className="flex flex-col gap-4"> // [!code highlight]
      <label>
        Name:
        <input type="text" name="name" required />
      </label>
      <label>
        File:
        <input type="file" name="file" required /> // [!code highlight]
      </label>
      <button type="submit" disabled={isPending}>
        Upload
      </button>
      {isPending && <div>Uploading...</div>}
      {data && <div>Success: {data}</div>}
      {error && <div>Error: {JSON.stringify(error.fieldErrors)}</div>}
    </form>
  );
}
```

In this example, we define a server action `uploadFile` that expects a `name` field of type `string` and a `file` field of type `File`. The `type` is set to `"formData"` to handle the form data.

On the client side, we include a file input with the name `"file"` in the form. When the form is submitted, the file is included in the `FormData` object and sent to the server action for processing.

Remember to handle the uploaded file appropriately in the server action's `handler` function based on your specific requirements.

That's it! You can now upload files using ZSA with forms.

## Multi Entry Forms

ZSA also supports multi entry forms. This means that you can have multiple input fields with the same name in a form. To handle multi entry forms, you can set the schema to an array of the expected type.

Here's an example of how to create a multi entry form using ZSA:

```ts title="actions.ts"
"use server"

import z from "zod"
import { createServerAction } from "zsa"

export const multiplyNumbersAction = createServerAction()
  .input(
    z.object({
      // an array of numbers
      number: z.array(z.coerce.number()), // [!code highlight]
      // an array of files
      filefield: z // [!code highlight]
        .array( // [!code highlight]
          z // [!code highlight]
            .instanceof(File) // [!code highlight]
            .refine( // [!code highlight]
              (file) => file.size > 0, // [!code highlight]
              "File cannot be empty" // [!code highlight]
            ) // [!code highlight]
            .refine( // [!code highlight]
              (file) => file.size < 1024, // [!code highlight]
              "File size must be less than 1kb" // [!code highlight]
            ) // [!code highlight]
        ) // [!code highlight]
    }),
    {
      type: "state",
    }
  )
  .handler(async ({ input }) => {
    await new Promise((resolve) => setTimeout(resolve, 500))
    return (
      input.number.reduce((a, b) => a * b, 1) +
      ` and got ${input.filefield.length} files`
    )
  })

```

Then on the client side, you can use the `useActionState` hook to handle the multi entry form:

```tsx title="multi-entry-example.tsx"
"use client"

import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { useActionState } from "react"
import { multiplyNumbersAction } from "./actions"

export default function MultiEntryExample() {
  let [[data, err], submitAction, isPending] = useActionState(
    multiplyNumbersAction,
    [null, null]
  )

  return (
    <Card className="not-prose">
      <CardHeader>
        <CardTitle>Multi Entry Form</CardTitle>
      </CardHeader>
      <CardContent className="flex flex-col gap-4">
        <form action={submitAction} className="flex flex-col gap-4">
          <Input name="number" placeholder="Enter number..." type="number" /> // [!code highlight]
          <Input name="number" placeholder="Enter number..." type="number" /> // [!code highlight]
          <Input name="filefield" type="file" multiple /> // [!code highlight]
          <Button disabled={isPending}>Multiply Numbers</Button>
        </form>
        {isPending && <div>Loading...</div>}
        {data && <p>Result: {data}</p>}
        {err && <div>Error: {JSON.stringify(err.fieldErrors)}</div>}
      </CardContent>
    </Card>
  )
}
```

Here is the result:

<ExampleComponent id="multi-entry" />

## Binding arguments

Sometimes you may want to bind arguments to the server action. This can be useful for passing additional data to the server action that won't be in your form. You can do this by passing an object to the `bind` option in the `useServerAction` hook.

```ts title="actions.ts"
"use server"

import z from "zod"
import { createServerAction } from "zsa"

export const produceNewMessage = createServerAction()
  .input(
    z.object({
      name: z.string().min(5),
      otherArg1: z.string(), // [!code highlight]
    }),
    {
      type: "formData",
    }
  )
  .handler(async ({ input }) => {
    await new Promise((resolve) => setTimeout(resolve, 500))
    return "Hello, " + input.name + " " + input.otherArg1
  })
```

Then on the client side, you can use the `useServerAction` hook to execute the server action and handle the result.

```tsx title="bind-form-example.tsx"
"use client"

import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { useServerAction } from "zsa-react"
import { produceNewMessage } from "./actions"

export const BindFormExample = () => {
  const { isPending, executeFormAction, isSuccess, data, isError, error } =
    useServerAction(produceNewMessage, {
      bind: { // [!code highlight]
        otherArg1: "binded", // [!code highlight]
      }, // [!code highlight]
    })

  return (
    <Card className="not-prose">
      <CardHeader>
        <CardTitle>Form Example</CardTitle>
      </CardHeader>
      <CardContent className="flex flex-col gap-4">
        <form className="flex flex-col gap-4" action={executeFormAction}>
          <Input type="text" name="name" placeholder="Enter your name..." />
          <Button className="w-full" type="submit" disabled={isPending}>
            Submit
          </Button>
          {isPending && <div>Loading...</div>}
          {isSuccess && <div>Success: {JSON.stringify(data)}</div>}
          {isError && <div>Error: {JSON.stringify(error.fieldErrors)}</div>}
        </form>
      </CardContent>
    </Card>
  )
}
```

Here is the result:

<ExampleComponent id="bind-form-example" />

### On Submit

If the action is a form action, you can also send more fields in the second argument of the `execute` function. This is useful for sending additional fields that are not part of the form.

```tsx title="bind-form-on-submit.tsx"
const { execute } = useServerAction(produceNewMessage)

return (
  <form
    onSubmit={async (event) => {
      event.preventDefault()
      const formData = new FormData(event.currentTarget)
      const [data, err] = await execute(formData, {
        otherArg1: "binded", // [!code highlight]
      })

      if (err) {
        // handle error
        return
      }
    }}
  />
)
```